Amazon SES でカスタムドメインを使用して受信したメールを S3 バケットに格納する構成を AWS CDK で実装してみた

Amazon SES でカスタムドメインを使用して受信したメールを S3 バケットに格納する構成を AWS CDK で実装してみた

Clock Icon2024.10.27

こんにちは、製造ビジネステクノロジー部の若槻です。

今回は、Amazon SES でカスタムドメインを使用して受信したメールを S3 バケットに格納する構成を AWS CDK で実装してみました。

やってみた

以下の構成を CDK で実装してみました。

前提

Amazon SES でのメール受信に使用したいカスタムドメインの DNS ホストゾーンが Route 53 に登録済みであることを前提とします。

CDK 実装

掲題の実装を行う CDK のコードです。

lib/main-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as ses from 'aws-cdk-lib/aws-ses';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as ses_actions from 'aws-cdk-lib/aws-ses-actions';
import * as cr from 'aws-cdk-lib/custom-resources';

export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const region = this.region;
    const domainName = process.env.DOMAIN_NAME || '';
    const noreplyEmailAddress = `noreply@${domainName}`;

    // 既存のホストゾーンを取得
    const hostedZone = route53.PublicHostedZone.fromLookup(this, 'HostedZone', {
      domainName,
    });

    // SES Domain Identity
    // MEMO: DKIM レコードの追加は既定で自動的に行われる
    new ses.EmailIdentity(this, 'SesEmailIdentity', {
      identity: ses.Identity.publicHostedZone(hostedZone),
    });

    // MXレコードの追加
    new route53.MxRecord(this, 'SESMXRecord', {
      zone: hostedZone,
      values: [
        {
          // @see https://docs.aws.amazon.com/general/latest/gr/ses.html#ses_inbound_endpoints
          hostName: `inbound-smtp.${region}.amazonaws.com`,
          priority: 10,
        },
      ],
      ttl: cdk.Duration.minutes(5),
    });

    // その他のレコードの追加は省略

    // 受信メール格納用バケット
    const incomingEmailBucket = new s3.Bucket(this, 'IncomingEmailBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // SES ルールセット
    // MEMO: ルールセットのアクティブ化は自動で行われない
    const ruleSet = new ses.ReceiptRuleSet(this, 'SESRuleSet', {
      rules: [
        {
          recipients: [noreplyEmailAddress],
          actions: [
            new ses_actions.S3({
              bucket: incomingEmailBucket,
              objectKeyPrefix: 'incoming/',
            }),
          ],
        },
      ],
    });

    // ルールセットをアクティブ化/非アクティブ化するカスタムリソース
    new cr.AwsCustomResource(this, 'SetActiveRuleSet', {
      onCreate: {
        service: 'SES',
        action: 'setActiveReceiptRuleSet',
        parameters: {
          RuleSetName: ruleSet.receiptRuleSetName,
        },
        physicalResourceId: cr.PhysicalResourceId.of('SetActiveRuleSet'),
      },
      onUpdate: {
        service: 'SES',
        action: 'setActiveReceiptRuleSet',
        parameters: {
          RuleSetName: ruleSet.receiptRuleSetName,
        },
        physicalResourceId: cr.PhysicalResourceId.of('SetActiveRuleSet'),
      },
      onDelete: {
        service: 'SES',
        action: 'setActiveReceiptRuleSet',
        parameters: {}, // 空のパラメータですべてのアクティブなルールセットを解除
      },
      policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
        resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
      }),
    });
  }
}

ポイントとしては作成した SES ルールセットのアクティブ化操作は ReceiptRuleSet コンストラクトクラスでは出来ないため、カスタムリソースを利用している点です。ルールセットの削除時も先に非アクティブ化を行う必要があります。ただし setActiveReceiptRuleSet アクションによる非アクティブ化はすべてのルールセットに適用されてしまうため、複数のルールセットを運用している場合は注意が必要です。

上記の実装を CDK Deploy コマンドによりデプロイします。

SES Domain Identity が作成される際に DKIM レコードが登録され、登録が成功したら AWS アカウント管理者宛に DKIM setup SUCCESS for xxxxxxxxxx.com in Asia Pacific (Tokyo) region という件名のメールが届きます。

デプロイ後のリソースの確認

作成されたルールセットです。カスタムリソースによりアクティブ化されています。

受信メール格納用バケットです。プレフィックスを見ると、セットアップ手順が記載されたオブジェクトが格納されていますが、こちらの対応は不要です。

メール受信の動作確認

適当なメーラー(今回は Gmail)で SES ルールセットの recipients に設定したメールアドレスにメールを送信します。

受信メール格納用バケットのプレフィックスにオブジェクトが格納されました。

オブジェクトの内容を確認すると、メールの内容が格納されていました。件名や本文は UTF-8 エンコードされた Base64 となっています。

Subject: =?UTF-8?B?44OG44K544OI5Lu25ZCN?=
To: [email protected]
Content-Type: multipart/alternative; boundary="00000000000005de8d062572b44a"

--00000000000005de8d062572b44a
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64

44OG44K544OI5pys5paHDQo=
--00000000000005de8d062572b44a
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: base64

PGRpdiBkaXI9Imx0ciI+44OG44K544OI5pys5paHPC9kaXY+DQo=
--00000000000005de8d062572b44a--

メール送信の動作確認

Domain Identity に設定したメールアドレスから SES でメールを送信してみます。

aws ses send-email \
    --from "test@${FROM_ADDRESS_DOMAIN}" \
    --destination "ToAddresses=${TO_ADDRESS}" \
    --message "Subject={Data=Test Email,Charset=utf-8},Body={Text={Data=This is a test email from AWS SES.,Charset=utf-8}}"

メールが受信できました。

SES ルールセットの削除動作の確認

カスタムリソースで設定した onDelete ハンドラによる SES ルールセットの非アクティブ化が正常に動作するか確認します。非アクティブ化されていないルールセットを削除しようとするとエラーが発生します。

ルールセットおよびカスタムリソースの記述を CDK スタックから削除して再度 CDK Deploy を行うと、エラー無くデプロイが完了し、ルールセットが削除されることが確認できました。

おわりに

Amazon SES でカスタムドメインを使用して受信したメールを S3 バケットに格納する構成を AWS CDK で実装してみました。

SES ルールセットのアクティブ化がカスタムリソースが無いと行えないというのはやはり GitHub でもずっと議論になっているようです。AWS システム内でメールルーティングの制御が必要などの理由で他の設定とは性質が異なるのでしょうか。
https://github.com/aws/aws-cdk/issues/10321
https://github.com/aws/aws-cdk/issues/28823

繰り返しになりますが今回のカスタムリソースによる方法は、非アクティブ化はすべてのルールセットに適用されてしまうという挙動となるため取り扱いには注意しましょう。

参考

https://dev.classmethod.jp/articles/ses-email-receive-tokyo/

以上

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.